const LinkedMap = require('./LinkedMap');
const util = require('./util');

class Node {
  constructor(filename) {
    this.filename = filename;
    this.next = [];
    this.badRequires = new Map();
    this.earlyAccess = new Map();
  }

  addNext(node) {
    this.next.push(node);
  }

  findCirculation(linkedMap) {
    if (linkedMap.has(this.filename)) {
      const circular = linkedMap.toArray(this.filename);
      if (!circular[circular.length - 1].isBadRequire(this)) {
        return [];
      }
      const badRequireChain = util.formatBadRequireChain(circular);
      return [
        {
          badRequireChain,
          reason: circular[circular.length - 2].getBadRequireReason(this.filename),
        },
      ];
    }
    const cs = [];
    linkedMap.set(this.filename, this);
    for (const next of this.next) {
      const c = next.findCirculation(linkedMap);
      c.forEach(item => {
        cs.push(item);
      });
    }
    linkedMap.delete(this.filename);
    return cs;
  }

  findNode(filename, set) {
    if (this.filename === filename) {
      return this;
    }
    set.add(this.filename);
    for (const node of this.next) {
      if (!set.has(node.filename)) {
        const res = node.findNode(filename, set);
        if (res) {
          return res;
        }
      }
    }
    return null;
  }

  addBadRequire(filename, exports) {
    this.badRequires.set(filename, exports);
  }

  addEarlyAccess(filename, key, value) {
    const props = this.earlyAccess.get(filename);
    if (!props) {
      this.earlyAccess.set(filename, [
        {
          key,
          value,
        }]);
    } else {
      props.push({
        key,
        value,
      });
    }
  }

  isBadRequire(node) {
    // 前后两次require得到的内容不同则视为循环引用导致的异常require
    // 在require后直接访问了导出对象的，也视为异常require：即虽然导出的是同一个对象，但是在依赖文件修改module.exports之前就已经访问了其中属性，仍然无法正确获取导出的内容
    return this.badRequires.get(node.filename) !== require(node.filename) || this.earlyAccess.has(node.filename);
  }

  getBadRequireReason(filename) {
    let reason = '';
    const actualExports = require(filename);
    if (this.badRequires.get(filename) !== actualExports) {
      reason += `- 对 "${filename}" 进行require得到的和实际导出的${util.underline('不是同一个对象')}\n`;
    } else {
      // 仅在访问的是同一个对象时，属性访问错误的提示才更有意义
      const earlyAccess = this.earlyAccess.get(filename);
      if (earlyAccess) {
        reason += `- 对 "${filename}" require结果的${util.underline('属性访问')}可能有问题(通常是在你访问后，该属性才被赋值；或是迭代该对象得到的结果不是预期):\n`;
        reason += `    访问的属性：${earlyAccess.map(item => item.key.toString()).join('、')}`
      }
    }
    return reason;
  }
}

class Graph {
  constructor(root) {
    this.root = new Node(root);
  }

  addEdge(from, to) {
    from = this.findNode(from) || new Node(from);
    to = this.findNode(to) || new Node(to);
    from.addNext(to);
    return {
      from,
      to,
    };
  }

  findNode(filename) {
    return this.root.findNode(filename, new Set());
  }

  findCirculation() {
    const cs = this.root.findCirculation(new LinkedMap());
    const uniqCs = [];
    for (const item of cs) {
      const c = item.badRequireChain
      if (!util.includesSameCirculation(uniqCs, c)) {
        uniqCs.push(item);
      }
    }
    return uniqCs;
  }
}

module.exports = Graph;
